DeriveState
State library that implements the Observer pattern, built with reactivity and composition in mind.
Heavily inspired from RxJS, but while RxJS focuses on asynchronous values, DeriveState focuses on stateful boxes, (aka cells in a spreadsheet) - As if everything were a BehaviourSubject
, and you create new states deriving them from other states.
Experimental
Usage
State
import { State } from 'derive-state';
const apples = new State(0);
apples.subscribe(apples => console.log(`We now have ${apples} apple(s)`));
apples.setValue(2);
console.log('apples: ' + apples.getValue());
The initial value is optional: if omitted the state will be empty
import { State } from 'derive-state';
const apples = new State();
apples.subscribe(apples => console.log(`We now have ${apples} apple(s)`));
expect(() => apples.getValue()).toThrow();
expect(apples.hasValue()).toBe(false);
apples.value.then(v => console.log(`Promise resolved with ${v}`));
apples.setValue(2);
expect(apples.hasValue()).toBe(true);
Subscriptions and state can both get cleaned up:
import { State } from 'derive-state';
const apples = new State(0);
const unsub1 = apples.subscribe(
apples => console.log(`We now have ${apples} apple(s)`)
() => console.log("complete")
);
unsub1();
apples.setValue(2);
apples.subscribe(
apples => console.log(`We now have ${apples} apple(s)`)
() => console.log("complete")
);
apples.close();
expect(() => apples.setValue(5)).toThrow();
console.log('apples: ' + apples.getValue());
apples.subscribe(
apples => console.log(`We now have ${apples} apple(s)`)
() => console.log("complete")
);
Stateless
An stateless observable doesn't hold any value.
It has two main ways it can be used:
Similar to an event emitter
import { Stateless } from 'derive-state';
const clicks = new Stateless();
clicks.emit('click');
clicks.subscribe(() => console.log('received a click'));
clicks.emit('click');
Derived from another observable
import { Stateless } from 'derive-state';
const apples = new State(2);
const squaredApples = new Stateless(obs => {});
const squaredApples = new Stateless(obs =>
apples.subscribe(apples => obs.next(apples * apples), obs.complete)
);
squaredApples.subscribe(apples =>
console.log(`We now have ${apples} squared apple(s)`)
);
const pipedSquaredApples = apples.pipe(map(apples => apples * apples));
A stateless is lazy: Meaning it won't run the derive function until someone subscribes to it.
A stateless is multicast: It will share the derive function along all the subscribers in the chain.
Stateless are designed to make composition easily: This way we can write operators that define their behaviour, and we can compose them easily with pipe
.
Note that this has an apparent issue:
const squaredApples = apples.pipe(map(apples => apples * apples));
squaredApples.subscribe(apples =>
console.log(`We now have ${apples} squared apple(s)`)
);
squaredApples.subscribe(apples =>
console.log(`We now have ${apples} squared apple(s)`)
);
This is due to the nature of squaredApples
being stateless and multicast: By the time the second subscriber comes in, apples
has already emitted its value, so it won't receive it. When apple
changes, both subscribers will receive the new value.
That's why usually you would capture
these stateless observables into stateful observables. The trade-off is that while stateless observables get cleaned up when they don't have subscriptions, stateful observables as they hold a value they need cleaning up, that's why you'd call capture
only after you've applied the composed operator chain
const pears = new State(10);
const totalFruits = combine([apples, pears])
.pipe(map(([apples, pears]) => apples + pears))
.capture();
totalFruits.subscribe(fruits => console.log(`We now have ${fruits} fruits`));
totalFruits.subscribe(fruits => console.log(`We now have ${fruits} fruits`));
Operators
Creation operators
just(value)
: Emits value
on the first subscription.combine(obj)
: subscribes to every observable in obj
, and emits the value of all of them in the same structure as obj
(works with arrays too)merge(array)
: subscribes to all the observables in array and emits every value from them.
Pipeable operators
map(fn)
: maps the values from the source stream by using the map function.filter(fn)
: filters changes based on the filter function.distinctUntilChanged()
: prevents emitting the same value twice in a row.switchMap(fn)
: flattens out the observable returned by the map function, unsubscribing from the previous ones.take(n)
: updates at most N times.skip(n)
: skips the first N values.withDefault(value)
: adds in a value if none is present.scan(accumulator, initialValue?)
: accumulates and emits values using the accumulator function